﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

using NanoMvvm.Collections;

namespace NanoMvvm
{
    public class SelectionViewModel<T> : PropertyObservableBase, IList<T> where T : class
    {
        private readonly ObservableSortedCollection<SelectionItemViewModel<T>> innerList;
        private readonly ObservableSortedCollection<SelectionItemViewModel<T>> selectedList;
        private SelectionPolicy selectionPolicy;

        private bool clearing;

        public SelectionViewModel(IComparer<T> comparer = null)
        {
            innerList = new ObservableSortedCollection<SelectionItemViewModel<T>>(
                    comparer != null ? new SelectionContainerComparer<T>(comparer) : null);
            selectedList =
                new ObservableSortedCollection<SelectionItemViewModel<T>>(
                    new DelegateComparer<SelectionItemViewModel<T>>((x, y) => x.Index - y.Index));
        }

        public event EventHandler<ItemEventArgs<T>> ItemSelected;

        public event EventHandler<ItemEventArgs<T>> ItemDeselected;

        public event EventHandler<ItemEventArgs<T>> ItemSelection;

        public event EventHandler<ItemOperationCancelEventArgs<T>> ItemDeselection;

        public event EventHandler<ItemEventArgs<T>> ItemAdded;

        public event EventHandler<ItemEventArgs<T>> ItemRemoved;

        public SelectionPolicy SelectionPolicy
        {
            get
            {
                return selectionPolicy;
            }

            set
            {
                if (value == selectionPolicy)
                {
                    return;
                }

                switch (value)
                {
                    case SelectionPolicy.Free:
                        break;

                    case SelectionPolicy.UpToOne:
                        if (selectedList.Count > 1)
                        {
                            if (!DeselectAllButFirst())
                            {
                                return;
                            }
                        }

                        break;

                    case SelectionPolicy.ExactlyOne:
                        if (selectedList.Count > 1)
                        {
                            if (!DeselectAllButFirst())
                            {
                                return;
                            }
                        }
                        else if (selectedList.Count == 0 && SelectableCount > 0)
                        {
                            ForceSelectFirst();
                        }

                        break;

                    case SelectionPolicy.AtLeastOne:
                        if (selectedList.Count == 0 && SelectableCount > 0)
                        {
                            ForceSelectFirst();
                        }

                        break;

                    case SelectionPolicy.None:
                        DeselectAll();
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }

                SetProperty(ref selectionPolicy, value, () => SelectionPolicy);
            }
        }

        public int FirstSelectionIndex
        {
            get
            {
                var holder = selectedList.FirstOrDefault();
                return holder != null ? holder.Index : -1;
            }

            set
            {
                if (value > innerList.Count)
                {
                    throw new IndexOutOfRangeException();
                }

                if (value == -1)
                {
                    DeselectAll();
                }

                innerList[value].IsSelected = true;
            }
        }

        public SelectionItemViewModel<T> FirstSelectedWrapper
        {
            get
            {
                return selectedList.FirstOrDefault();
            }
        }

        public T FirstSelectedItem
        {
            get
            {
                return selectedList.Count > 0 ? SelectedItems[0].Item : null;
            }
        }

        public IReadOnlyObservableList<SelectionItemViewModel<T>> Items
        {
            get
            {
                return innerList;
            }
        }

        public IReadOnlyObservableList<SelectionItemViewModel<T>> SelectedItems
        {
            get
            {
                return selectedList;
            }
        }

        public int Count
        {
            get
            {
                return innerList.Count;
            }
        }

        public int SelectableCount
        {
            get
            {
                return innerList.Count(o => o.IsSelectable);
            }
        }

        public bool IsReadOnly
        {
            get
            {
                return false;
            }
        }

        public T this[int index]
        {
            get
            {
                return innerList[index].Item;
            }

            set
            {
                Insert(index, value);
            }
        }

        IEnumerator<T> IEnumerable<T>.GetEnumerator()
        {
            return innerList.Select(o => o.Item).GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return ((IEnumerable<T>)this).GetEnumerator();
        }

        public void Add(T item)
        {
            ProcessAddAt(innerList.Count, item);
        }

        public void Add(T item, bool isSelectable)
        {
            ProcessAddAt(innerList.Count, item, isSelectable);
        }

        public void Clear()
        {
            clearing = true;
            try
            {
                for (int i = innerList.Count - 1; i >= 0; i--)
                {
                    ProcessRemoveAt(i);
                }
            }
            finally
            {
                clearing = false;
            }
        }

        public bool Contains(T item)
        {
            return innerList.Any(o => Equals(o.Item, item));
        }

        public void CopyTo(T[] array, int arrayIndex)
        {
            innerList.Select(o => o.Item).ToList().CopyTo(array, arrayIndex);
        }

        public bool Remove(T item)
        {
            for (int i = 0; i < innerList.Count; i++)
            {
                if (!Equals(innerList[i].Item, item))
                {
                    continue;
                }

                ProcessRemoveAt(i);
                return true;
            }

            return false;
        }

        public bool TrySelectAt(int index)
        {
            if (Count > index && Items[index].IsSelectable)
            {
                Items[index].IsSelected = true;
                if (Items[index].IsSelected)
                {
                    return true;
                }
            }

            return false;
        }

        public bool TryDeselectAt(int index)
        {
            if (Count > index && Items[index].IsSelected)
            {
                Items[index].IsSelected = false;
                if (!Items[index].IsSelected)
                {
                    return true;
                }
            }

            return false;
        }

        public void TrySelectAll()
        {
            for (int i = innerList.Count - 1; i >= 0; i--)
            {
                innerList[i].IsSelected = true;
            }
        }

        public void TryDeselectAll()
        {
            for (int i = innerList.Count - 1; i >= 0; i--)
            {
                innerList[i].IsSelected = false;
            }
        }

        public int IndexOf(T item)
        {
            for (int i = 0; i < innerList.Count; i++)
            {
                if (Equals(item, innerList[i].Item))
                {
                    return i;
                }
            }

            return -1;
        }

        public void Insert(int index, T item)
        {
            ProcessAddAt(index, item);
        }

        public void Insert(int index, T item, bool selectable)
        {
            ProcessAddAt(index, item, selectable);
        }

        public void RemoveAt(int index)
        {
            ProcessRemoveAt(index);
        }

        public void TrySelectFirst()
        {
            foreach (var o in innerList)
            {
                if (!o.IsSelectable)
                {
                    continue;
                }

                o.IsSelected = true;

                break;
            }
        }

        internal void ProcessItemSelectionRequest(SelectionItemViewModel<T> holder)
        {
            if (!holder.IsSelectable)
            {
                return;
            }

            switch (SelectionPolicy)
            {
                case SelectionPolicy.ExactlyOne:
                case SelectionPolicy.UpToOne:
                    if (selectedList.Count > 0)
                    {
                        if (!DeselectAll())
                        {
                            return;
                        }
                    }

                    break;

                case SelectionPolicy.Free:
                case SelectionPolicy.AtLeastOne:
                    break;

                case SelectionPolicy.None:
                    return;
                default:
                    throw new ArgumentOutOfRangeException();
            }

            SelectItem(holder, false);
        }

        internal void ProcessItemDeselectionRequest(SelectionItemViewModel<T> holder)
        {
            if (!holder.IsSelectable)
            {
                return;
            }

            switch (SelectionPolicy)
            {
                case SelectionPolicy.ExactlyOne:
                case SelectionPolicy.AtLeastOne:
                    if (selectedList.Count <= 1)
                    {
                        return;
                    }

                    break;

                case SelectionPolicy.UpToOne:
                case SelectionPolicy.Free:
                case SelectionPolicy.None:
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }

            DeselectItem(holder, false, false);
        }

        internal void ProcessItemEnable(SelectionItemViewModel<T> holder)
        {
            switch (SelectionPolicy)
            {
                case SelectionPolicy.ExactlyOne:
                case SelectionPolicy.AtLeastOne:
                    if (selectedList.Count == 0)
                    {
                        SelectItem(holder, true);
                    }

                    break;

                case SelectionPolicy.UpToOne:
                case SelectionPolicy.Free:
                case SelectionPolicy.None:
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }

            OnItemEnabled(holder);
        }

        internal void ProcessItemDisable(SelectionItemViewModel<T> holder)
        {
            if (!holder.IsSelected)
            {
                OnItemDisabled(holder);
                return;
            }

            DeselectItem(holder, true, true);

            switch (SelectionPolicy)
            {
                case SelectionPolicy.ExactlyOne:
                case SelectionPolicy.AtLeastOne:
                    if (selectedList.Count == 0 && SelectableCount > 1)
                    {
                        foreach (var o in innerList)
                        {
                            if (!o.IsSelectable || Equals(o, holder))
                            {
                                continue;
                            }

                            SelectItem(o, true);
                            break;
                        }
                    }

                    break;

                case SelectionPolicy.UpToOne:
                case SelectionPolicy.Free:
                case SelectionPolicy.None:
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }

            OnItemDisabled(holder);
        }

        protected virtual void OnItemSelection(SelectionItemViewModel<T> holder, bool automatic)
        {
            var handler = ItemSelection;
            if (handler != null)
            {
                handler(this, new ItemEventArgs<T>(holder.Item, automatic, clearing));
            }
        }

        protected virtual void OnItemSelected(SelectionItemViewModel<T> holder, bool automatic)
        {
            var handler = ItemSelected;
            if (handler != null)
            {
                handler(this, new ItemEventArgs<T>(holder.Item, automatic, clearing));
            }
        }

        protected virtual bool OnItemDeselection(SelectionItemViewModel<T> holder, bool automatic)
        {
            var handler = ItemDeselection;
            if (handler != null)
            {
                var args = new ItemOperationCancelEventArgs<T>(holder.Item, automatic, clearing);
                handler(this, args);
                return !args.Cancel;
            }

            return true;
        }

        protected virtual void OnItemDeselected(SelectionItemViewModel<T> holder, bool automatic)
        {
            var handler = ItemDeselected;
            if (handler != null)
            {
                handler(this, new ItemEventArgs<T>(holder.Item, automatic, clearing));
            }
        }

        protected virtual void OnItemEnabled(SelectionItemViewModel<T> holder)
        {
        }

        protected virtual void OnItemDisabled(SelectionItemViewModel<T> holder)
        {
        }

        protected virtual SelectionItemViewModel<T> CreateSelectionItem(T item)
        {
            return new SelectionItemViewModel<T>(this, item);
        }

        private void ForceSelectFirst()
        {
            foreach (var o in innerList)
            {
                if (!o.IsSelectable)
                {
                    continue;
                }

                SelectItem(o, true);

                break;
            }
        }

        private bool DeselectAllButFirst()
        {
            for (int i = selectedList.Count - 1; i >= 1; i--)
            {
                if (!DeselectItem(selectedList[i], false, true))
                {
                    return false;
                }
            }

            return true;
        }

        private void ProcessAddAt(int index, T item, bool selectable = true)
        {
            var holder = CreateSelectionItem(item);
            innerList.Insert(index, holder);

            RecheckIndexes();

            if (!selectable)
            {
                holder.IsSelectable = false;
            }
            else
            {
                switch (SelectionPolicy)
                {
                    case SelectionPolicy.ExactlyOne:
                    case SelectionPolicy.AtLeastOne:
                        if (SelectableCount == 1)
                        {
                            SelectItem(holder, true);
                        }

                        break;

                    case SelectionPolicy.Free:
                    case SelectionPolicy.UpToOne:
                    case SelectionPolicy.None:
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }

            var handler = ItemAdded;
            if (handler != null)
            {
                handler(this, new ItemEventArgs<T>(item, false));
            }
        }

        private void ProcessRemoveAt(int index)
        {
            var holder = innerList[index];

            if (holder.IsSelected)
            {
                DeselectItem(holder, true, true);
            }

            innerList.RemoveAt(index);

            RecheckIndexes();

            if (holder.IsSelectable)
            {
                switch (SelectionPolicy)
                {
                    case SelectionPolicy.ExactlyOne:
                    case SelectionPolicy.AtLeastOne:
                        if (selectedList.Count == 0 && SelectableCount > 0)
                        {
                            ForceSelectFirst();
                        }

                        break;

                    case SelectionPolicy.Free:
                    case SelectionPolicy.UpToOne:
                    case SelectionPolicy.None:
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }

            var handler = ItemRemoved;
            if (handler != null)
            {
                handler(this, new ItemEventArgs<T>(holder.Item, false, clearing));
            }

            holder.Index = -1;
        }

        private void RecheckIndexes()
        {
            for (int i = 0; i < innerList.Count; i++)
            {
                innerList[i].Index = i;
            }
        }

        private bool DeselectAll()
        {
            for (int i = selectedList.Count - 1; i >= 0; i--)
            {
                if (!DeselectItem(selectedList[i], false, true))
                {
                    return false;
                }
            }

            return true;
        }

        private void SelectItem(SelectionItemViewModel<T> holder, bool automatic)
        {
            if (holder.IsSelected)
            {
                return;
            }

            holder.ProcessSelection();
            OnItemSelection(holder, automatic);

            holder.SetIsSelectedInternal(true);
            selectedList.Add(holder);

            holder.ProcessSelected();

            NotifyPropertyChanged(() => FirstSelectionIndex);
            NotifyPropertyChanged(() => FirstSelectedWrapper);
            NotifyPropertyChanged(() => FirstSelectedItem);

            OnItemSelected(holder, automatic);
        }

        private bool DeselectItem(SelectionItemViewModel<T> holder, bool force, bool automatic)
        {
            if (!holder.IsSelected)
            {
                return true;
            }

            if (!holder.ProcessDeselection() && !force)
            {
                return false;
            }

            if (!OnItemDeselection(holder, automatic) && !force)
            {
                return false;
            }

            holder.SetIsSelectedInternal(false);
            selectedList.Remove(holder);

            holder.ProcessDeselected();
            
            NotifyPropertyChanged(() => FirstSelectionIndex);
            NotifyPropertyChanged(() => FirstSelectedWrapper);
            NotifyPropertyChanged(() => FirstSelectedItem);

            OnItemDeselected(holder, automatic);

            return true;
        }

        private class SelectionContainerComparer<T1> : IComparer<SelectionItemViewModel<T1>> where T1 : class
        {
            private readonly IComparer<T1> internalComparer;

            public SelectionContainerComparer(IComparer<T1> internalComparer)
            {
                this.internalComparer = internalComparer;
            }

            public int Compare(SelectionItemViewModel<T1> x, SelectionItemViewModel<T1> y)
            {
                return internalComparer.Compare(x.Item, y.Item);
            }
        }
    }
}